fix(hooks): make the dirty list mean "edited since last publish" (#4 + #5) - #11
Merged
Conversation
`.cofoundy/` holds an agent's kickoff prompt, task specs and run state. It was untracked but NOT ignored, so a single `git add -A` would publish machine paths and internal tooling detail into a public history — where a push is not undone, it is archived. Closing this by infrastructure rather than by the committer's attention.
…#5) `stop.sh` asserted "this doc is unpublished" from a measurement of "this doc was edited". Those are different propositions, and two flows sat entirely inside the gap: #4 — repo-backed vault: the mandated flow is commit + push → the repo's Action publishes. Nothing local ever cleared the list, so the nag was unconditional. #5 — the agent runs `basalt publish <file>` mid-turn, which is exactly what the nudge asks for, and `stop.sh` was the only writer that cleared the list. The nudge could not be satisfied except by not editing. Mechanism A — publish-tracking. New `post-publish.sh`, a PostToolUse(Bash) hook mirroring `post-edit.sh`. The dirty list becomes an append-only journal: edits append absolute paths, publishes append `-<operand>` tombstones, and Stop replays it. No `basalt status` call (network in the hot path, and the same issue documents that oracle as lying on image-bearing docs); success semantics come from the measured harness fact that PostToolUse fires only on exit 0. Only the last segment of a compound command, or an all-`&&` chain, may clear anything; a quote, backslash or newline anywhere clears nothing; `--dry-run` clears nothing. `post-edit.sh` changes in exactly one way: the dirty append is now unconditional. `append_once` dedups against history, and under a journal a tombstoned path stays in that history forever — so a re-edit's append was skipped and Stop went silent on a re-dirtied doc. `append_once` stays on the orphan list, where the dedup is semantically required. Mechanism B — a `stop.sh` git-state filter for the #4 case. All local (`git` plumbing + `case` globs, no regex, zero network). A doc is dropped only on positive local evidence that a push of this file, on this branch, starts a Basalt publish workflow: the candidate workflow is itself tracked, clean and pushed; the doc has no uncommitted changes and its last commit is an ancestor of upstream; the branch is the repo's default branch; the workflow has a `push:` trigger, every `branches:` list contains the branch, every `paths:` list matches, and nothing negates. Every failure, gap and ambiguity keeps the nag. Run-step selection and a workflow that fails at runtime stay invisible — a declared residual, not a closed one. Tests: 83 assertions, plus an opt-in mutation pass (`tests/mutate.sh`, 15 mutants, all killed) and `tests/probe-harness.sh`, which re-measures the exit-0 firing policy the design rests on and which the suite cannot pin. Also sanitizes five already-public fixtures carrying a machine path, an internal product path and real session UUIDs. Containment, not prevention — the history stays public.
This was referenced Aug 17, 2026
The plugin treats
.mdx as the primary publish signal; the emitted CI template publishes .md only
#13
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4. Closes #5.
The proposition the hook got wrong
stop.shasserted "this doc is unpublished" from a measurement of "this doc was edited". Those are different propositions, and two flows sat entirely inside the gap:statusmiente en docs con imágenes #5 — the agent runsbasalt publish <file>mid-turn, which is exactly what the nudge asks for, andstop.shwas the only writer that ever cleared the list.So the agent had no way to satisfy the hook except by not editing. A nudge that cannot be satisfied is not a nudge — it trains the reader to mute the channel, and then the sensor is worth less than nothing.
The list now means: vault docs edited since their last publish.
Two mechanisms, both local — no network added to the hot path
A —
post-publish.sh(new),PostToolUseonBash. Edit-tracking's twin: it appends a tombstone for a doc a command really published. The list became an append-only journal (post-edit.shappends paths,post-publish.shappends-<path>,stop.shreplays), so ordering is correct by construction and twoO_APPENDwriters cannot lose each other's updates.B — a git-state filter in
stop.sh. Nine conditions, all local. Its claim is deliberately narrow: positive local evidence that a push of this file, on this branch, starts a Basalt publish workflow. It does not claim to predict what the Action publishes — the real publish set is decided by agit ls-files | xargspipeline and filename conventions in shell, which no hook can model. That limit is written into the code, not left implied.Why not
basalt statusIt is the obvious fix and it is rejected on two independent grounds. It is network on every Stop with any dirty file, which #4's guardrail closes textually. And #5's own body documents it lying: publish rewrites image refs without touching the local file, so
statusreturnsmodifiedimmediately after a successful publish for any doc with local images — nearly every doc in the vault where the bug was filed. The fix would have failed exactly where it was reported, and failed silently.Measured, not assumed
Three harness facts this rests on were captured, because none were previously verified — the existing capability notes cover
Edit|Write;Bashhad never been captured.PostToolUse(Bash)fires only on exit 0. Measured with a control counting executions separately from hook fires: 4 executions, 2 fires. A failed publish never reaches the hook, which is how success is known without an exit-code field (the payload has none) and without a network call.cwddoes not follow a compoundcd.cd /tmp && pwdprints/tmp; the payload still reports the session dir. Since El Stop hook pide publicar docs ya publicados (dirty-list sin publish-state) +statusmiente en docs con imágenes #5's own repro iscd <vault> && basalt publish …, resolving operands againstcwdalone would match nothing — and a hook that matches nothing is indistinguishable from a dead one.ugrepand GNUgrepdisagree on[^\n]inside a bracket. Local dev and CI would diverge, so there is no regex path parsing here —caseglobs and string operators only.Everything ambiguous fails toward the nag
--dry-run·publish f || trueand non-final;segments (exit 0 is a property of the command, not a segment) · quoted or multi-line commands · unresolvable operands · a doc whose extension the workflow'spaths:list does not match · a workflow that is untracked, dirty, unpushed, has nopush:trigger, usespaths-ignore:or!negation, or is gated on another branch.Verification
scripts/validate-skills.shgreen.tests/mutate.sh, opt-in, not CI): 15 mutants, 15 killed, 0 survived. Each mutant is proven live — patch lands,bash -n, smoke run — before its verdict is read, so "the test does not catch it" cannot be confused with "the mutant never ran". The first run killed 13 and two survived: one condition was shadowed by another, and one guard was defended in three places so removing a single layer changed nothing. Both are now isolated and killed.statusmiente en docs con imágenes #5 verbatim. Critically, the negative arm behaves identically before and after: "edited and not committed" still nags. The feature was repaired, not deleted.Explicitly not fixed here
basalt statusreportsmodifiedafter a successful publish for docs with local images (El Stop hook pide publicar docs ya publicados (dirty-list sin publish-state) +statusmiente en docs con imágenes #5 problem 2) → basalt status reportsmodifiedright after a successful publish for docs with local images #12. The comparator lives in the CLI, not this repo.publish_doctakes title + markdown rather than a path. A measurement gap, not a construction — and its failure direction is a surviving nag, never silence..mdxas the primary intent-to-publish signal while the CI template the CLI emits publishes.mdonly → The plugin treats.mdxas the primary publish signal; the emitted CI template publishes.mdonly #13. This PR's hook is correct under both shapes because it evaluates the workflow's actualpaths:list, but it does not resolve the product-level disagreement.